import { LinkedHashMap } from './Tiles/utils/linked-hash-map';
import { GltfObject } from './Tiles/decoder/GltfObject';
import { Matrix4 } from '../../../core/math/Matrix4';
import { Evented } from '../../../event/index.js';
import { DBHelper } from '../../indexdb/DBHelper';
import { process } from "../../../process/worker/process";
import { Engine } from '../../../Engine';
import { resolveImplicite } from './implicit/ImplicitTileResolver.js';
import { generateUUID } from "../../../core/math/MathUtils.js";
import { ComponentType } from '../../../component/index.js';
import { Register } from '../../../register/Register.js';

let concurrentDownloads = 0;
let concurrentLoads = 0;
const zUpToYUpMatrix = new Matrix4();
zUpToYUpMatrix.set(1, 0, 0, 0,
    0, 0, -1, 0,
    0, 1, 0, 0,
    0, 0, 0, 1);

class TileLoader extends Evented {
    constructor(options) {
        super();
        this.proxy = options.proxy;
        this.maxCachedItems = options.maxCachedItems
        this.geometricErrorMultiplier = options.geometricErrorMultiplier
        this.indexedDB = options.indexedDB
        this.back = options.back
        this.translation = options.translation
        this.crs = options.crs
        if (this.indexedDB) {
            DBHelper.getInstance().open("3dtiles", (e) => {
                this.db = e
            })
        }
        this.cache = new LinkedHashMap();
        this.register = {};
        this.ready = [];
        this.downloads = [];
        this.nextReady = [];
        this.nextDownloads = [];

    }


    update() {
        const self = this;
        if (concurrentDownloads < 8) {
            self.download();
        }
        self.loadBatch();
    }

    scheduleDownload(f) {
        this.downloads.unshift(f);
    }
    download() {
        if (this.nextDownloads.length == 0) {
            this.getNextDownloads();
            if (this.nextDownloads.length == 0) return;
        }
        while (this.nextDownloads.length > 0) {
            const nextDownload = this.nextDownloads.shift();
            if (!!nextDownload && nextDownload.shouldDoDownload()) {
                nextDownload.doDownload();
            }
        }
        return;
    }
    meshReceived(cache, register, key, distanceFunction, getSiblings, level, uuid) {
        this.ready.unshift([cache, register, key, distanceFunction, getSiblings, level, uuid]);
    }
    loadBatch() {
        if (this.nextReady.length == 0) {
            this.getNextReady();
            if (this.nextReady.length == 0) return 0;
        }
        const data = this.nextReady.shift();
        if (!data) return 0;
        const cache = data[0];
        const register = data[1];
        const key = data[2];
        const mesh = cache.get(key);

        if (!!mesh && !!register[key]) {
            Object.keys(register[key]).forEach(tile => {
                const callback = register[key][tile];
                if (!!callback) {
                    callback(mesh);
                    register[key][tile] = null;
                }
            });
        }
        return 1;
    }

    getNextDownloads() {
        let smallestDistance = Number.MAX_VALUE;
        let closest = -1;
        for (let i = this.downloads.length - 1; i >= 0; i--) {
            if (!this.downloads[i].shouldDoDownload()) {
                this.downloads.splice(i, 1);
                continue;
            }
            if (!this.downloads[i].distanceFunction) {
                this.nextDownloads.push(this.downloads.splice(i, 1)[0]);
            }
        }
        if (this.nextDownloads.length > 0) return;
        for (let i = this.downloads.length - 1; i >= 0; i--) {
            const dist = this.downloads[i].distanceFunction() * this.downloads[i].level;
            if (dist < smallestDistance) {
                smallestDistance = dist;
                closest = i;
            }
        }
        if (closest >= 0) {
            const closestItem = this.downloads.splice(closest, 1).pop();
            this.nextDownloads.push(closestItem);
            const siblings = closestItem.getSiblings();
            for (let i = this.downloads.length - 1; i >= 0; i--) {
                if (siblings.map(s => s.uuid).includes(this.downloads[i].uuid)) {
                    this.nextDownloads.push(this.downloads.splice(i, 1).pop());
                }
            }
        }
    }
    getNextReady() {
        let smallestDistance = Number.MAX_VALUE;
        let closest = -1;
        for (let i = this.ready.length - 1; i >= 0; i--) {
            if (!this.ready[i][3]) {
                this.nextReady.push(this.ready.splice(i, 1)[0]);
            }
        }
        if (this.nextReady.length > 0) return;
        for (let i = this.ready.length - 1; i >= 0; i--) {
            const dist = this.ready[i][3]() * this.ready[i][5];
            if (dist < smallestDistance) {
                smallestDistance = dist;
                closest = i
            }
        }
        if (closest >= 0) {
            const closestItem = this.ready.splice(closest, 1).pop();
            this.nextReady.push(closestItem);
        }
    }
    get(abortController, tileIdentifier, path, callback, distanceFunction, getSiblings, level, worldMat) {
        const self = this;
        const key = simplifyPath(path);
        const realAbortController = new AbortController();
        abortController.signal.addEventListener("abort", () => {
            if (!self.register[key] || Object.keys(self.register[key]).length == 0) {
                realAbortController.abort();
            }
        })
        if (!path.includes(".b3dm") && !path.includes(".json") && !path.includes(".gltf") && !path.includes(".glb")) {
            console.error("后缀名不符合");
            return;
        }
        if (!self.register[key]) {
            self.register[key] = {};
        }
        if (!!self.register[key][tileIdentifier]) {
            console.error(" 一个tile应该只加载一次");
        }
        self.register[key][tileIdentifier] = callback;

        const cachedObject = self.cache.get(key);
        if (!!cachedObject) {
            this.meshReceived(self.cache, self.register, key, distanceFunction, getSiblings, level, tileIdentifier);
        } else if (Object.keys(self.register[key]).length == 1) {//前面加了self.register[key][tileIdentifier] = callback;
            let downloadFunction;
            const entity = Register.instance.entityManager.get(key)
            if (entity && entity.id) {//如果没有被空闲任务销毁，那么需要拉回来
                if (!entity.visibility) {
                    entity.setVisibility(true)
                }
                entity.removeComponent(ComponentType.Destroy)
                concurrentDownloads++;
                self.cache.put(key, entity);
                self.checkSize();
                this.meshReceived(self.cache, self.register, key, distanceFunction, getSiblings, level, tileIdentifier);
                concurrentDownloads--;
            } else {
                if (path.includes(".b3dm")) {
                    downloadFunction = () => {
                        concurrentDownloads++;
                        if (this.db) {//开启indexDB
                            this.db.get(path, event => {
                                const gltf = event.result
                                if (gltf) {
                                    const objectGlb = new GltfObject(gltf.value, key,worldMat);
                                    self.cache.put(key, objectGlb.b3dmNode);
                                    self.checkSize();
                                    this.meshReceived(self.cache, self.register, key, distanceFunction, getSiblings, level, tileIdentifier);
                                    concurrentDownloads--;
                                } else {
                                    let data = { key, path, type: "b3dm", back: self.back ,matrix:worldMat}
                                    process(data, (message) => {
                                        let gltf = message.data;
                                        if (gltf) {
                                            const key = data.key;
                                            const objectGlb = new GltfObject(gltf, key,worldMat)
                                            self.cache.put(key, objectGlb.b3dmNode);
                                            self.checkSize();
                                            this.meshReceived(self.cache, self.register, key, distanceFunction, getSiblings, level, tileIdentifier);
                                            //添加写入indexDB任务
                                            this.db.add({ "id": path, "value": gltf })
                                            concurrentDownloads--;
                                        }
                                    })
                                }
                            })
                        } else {//没有开启
                            let data = { key, path, type: "b3dm", back: self.back,matrix:worldMat}
                            process(data, (message) => {
                                let gltf = message.data;
                                if (gltf) {
                                    const key = data.key;
                                    const objectGlb = new GltfObject(gltf, key,worldMat)
                                    self.cache.put(key, objectGlb.b3dmNode);
                                    self.checkSize();
                                    self.meshReceived(self.cache, self.register, key, distanceFunction, getSiblings, level, tileIdentifier);
                                    concurrentDownloads--;
                                }
                            })
                        }
                    }
                } else if (path.includes(".glb") || path.includes(".gltf")) {
                    downloadFunction = () => {
                        concurrentDownloads++;
                        if (this.db) {//开启indexDB
                            this.db.get(path, event => {
                                const gltf = event.result
                                if (gltf) {
                                    const objectGlb = new GltfObject(gltf.value, key,worldMat);
                                    self.cache.put(key, objectGlb.b3dmNode);
                                    self.checkSize();
                                    this.meshReceived(self.cache, self.register, key, distanceFunction, getSiblings, level, tileIdentifier);
                                    concurrentDownloads--;
                                } else {
                                    let data = { key, path, type: "glb", back: self.back,matrix:worldMat }
                                    process(data, (message) => {
                                        let gltf = message.data;
                                        if (gltf) {
                                            const key = data.key;
                                            const objectGlb = new GltfObject(gltf, key,worldMat)
                                            self.cache.put(key, objectGlb.b3dmNode);
                                            self.checkSize();
                                            self.meshReceived(self.cache, self.register, key, distanceFunction, getSiblings, level, tileIdentifier);
                                            //添加写入indexDB任务
                                            this.db.add({ "id": path, "value": gltf })
                                            concurrentDownloads--;
                                        }
                                    })
                                }
                            })
                        } else {//没有开启
                            let data = { key, path, type: "glb", back: self.back,matrix:worldMat }
                            process(data, (message) => {
                                let gltf = message.data;
                                if (gltf) {
                                    const key = data.key;
                                    const objectGlb = new GltfObject(gltf, key,worldMat)
                                    self.cache.put(key, objectGlb.b3dmNode);
                                    self.checkSize();
                                    self.meshReceived(self.cache, self.register, key, distanceFunction, getSiblings, level, tileIdentifier);
                                    concurrentDownloads--;
                                }
                            })
                        }

                    }
                } else if (path.includes(".json")) {
                    downloadFunction = () => {
                        if (this.db) {//开启indexDB
                            this.db.get(path, event => {
                                const result = event.result
                                if (result) {
                                    let json = result.value
                                    resolveImplicite(json, path).then(json => {
                                        self.cache.put(key, json);
                                        self.checkSize();
                                        self.meshReceived(self.cache, self.register, key);
                                        concurrentDownloads--;
                                    })
                                } else {
                                    const data = { key, path, type: "json", back: self.back, indexedDB: true, }
                                    process(data, (message) => {
                                        let json = message.data;
                                        resolveImplicite(json, path).then(json => {
                                            self.cache.put(key, json);
                                            self.checkSize();
                                            self.meshReceived(self.cache, self.register, key);
                                            concurrentDownloads--;
                                        })
                                    })
                                }
                            })
                        } else {
                            const data = { key, path, type: "json", back: self.back, indexedDB: false, };
                            process(data, (message) => {
                                let json = message.data;
                                resolveImplicite(json, path).then(json => {
                                    self.cache.put(key, json);
                                    self.checkSize();
                                    self.meshReceived(self.cache, self.register, key);
                                    concurrentDownloads--;
                                })
                            })
                        }
                    }
                }
                this.scheduleDownload({
                    "shouldDoDownload": () => {
                        return !abortController.signal.aborted && !!self.register[key] && Object.keys(self.register[key]).length > 0;
                    },
                    "doDownload": downloadFunction,
                    "distanceFunction": distanceFunction,
                    "getSiblings": getSiblings,
                    "level": level,
                    "uuid": tileIdentifier
                })
            }

        }
    }



    invalidate(path, tileIdentifier) {
        const key = simplifyPath(path);
        if (!!this.register[key]) {
            delete this.register[key][tileIdentifier];
        }
    }

    checkSize() {
        const self = this;
        let i = 0;
        while (self.cache.size() > self.maxCachedItems && i < self.cache.size()) {
            i++;
            const entry = self.cache.head();
            const reg = self.register[entry.key];
            if (!!reg) {
                if (Object.keys(reg).length > 0) {
                    self.cache.remove(entry.key);
                    self.cache.put(entry.key, entry.value);
                } else {
                    self.cache.remove(entry.key);
                    delete self.register[entry.key];
                    const b3dmNode = entry.value
                    b3dmNode.destroy(true)
                }
            }

        }
    }
}

function setIntervalAsync(fn, delay) {
    let timeout;

    const run = async () => {
        const startTime = Date.now();
        try {
            await fn();
        } catch (err) {
            console.error(err);
        } finally {
            const endTime = Date.now();
            const elapsedTime = endTime - startTime;
            const nextDelay = elapsedTime >= delay ? 0 : delay - elapsedTime;
            timeout = setTimeout(run, nextDelay);
        }
    };

    timeout = setTimeout(run, delay);

    return { clearInterval: () => clearTimeout(timeout) };
}


function simplifyPath(main_path) {

    var parts = main_path.split('/'),
        new_path = [],
        length = 0;
    for (var i = 0; i < parts.length; i++) {
        var part = parts[i];
        if (part === '.' || part === '' || part === '..') {
            if (part === '..' && length > 0) {
                length--;
            }
            continue;
        }
        new_path[length++] = part;
    }

    if (length === 0) {
        return '/';
    }

    var result = '';
    for (var i = 0; i < length; i++) {
        result += '/' + new_path[i];
    }

    return result;
}

export { TileLoader };